﻿using System;
using System.Collections.Generic;

using System.Text;
using System.Reflection;
using System.Security;

namespace RoughJs.Ast
{
    /// <summary>
    /// 2010-01-5 ----- 2010-01-29
    /// 陈曦 1.0
    /// Microsoft Public License (Ms-PL)
    ///This license governs use of the accompanying software. If you use the software, you accept this license. If you do not accept the license, do not use the software.
    ///1. Definitions
    ///The terms "reproduce," "reproduction," "derivative works," and "distribution" have the same meaning here as under U.S. copyright law.
    ///A "contribution" is the original software, or any additions or changes to the software.
    ///A "contributor" is any person that distributes its contribution under this license.
    ///"Licensed patents" are a contributor's patent claims that read directly on its contribution.
    ///2. Grant of Rights
    ///(A) Copyright Grant- Subject to the terms of this license, including the license conditions and limitations in section 3, each contributor grants you a non-exclusive, worldwide, royalty-free copyright license to reproduce its contribution, prepare derivative works of its contribution, and distribute its contribution or any derivative works that you create.
    ///(B) Patent Grant- Subject to the terms of this license, including the license conditions and limitations in section 3, each contributor grants you a non-exclusive, worldwide, royalty-free license under its licensed patents to make, have made, use, sell, offer for sale, import, and/or otherwise dispose of its contribution in the software or derivative works of the contribution in the software.
    ///3. Conditions and Limitations
    ///(A) No Trademark License- This license does not grant you rights to use any contributors' name, logo, or trademarks.
    ///(B) If you bring a patent claim against any contributor over patents that you claim are infringed by the software, your patent license from such contributor to the software ends automatically.
    ///(C) If you distribute any portion of the software, you must retain all copyright, patent, trademark, and attribution notices that are present in the software.
    ///(D) If you distribute any portion of the software in source code form, you may do so only under this license by including a complete copy of this license with your distribution. If you distribute any portion of the software in compiled or object code form, you may only do so under a license that complies with this license.
    ///(E) The software is licensed "as-is." You bear the risk of using it. The contributors give no express warranties, guarantees or conditions. You may have additional consumer rights under your local laws which this license cannot change. To the extent permitted under your local laws, the contributors exclude the implied warranties of merchantability, fitness for a particular purpose and non-infringement. 
    /// </summary>
    public class FunctionObject : BaseFunction
    {

        public static Object convertArg(Context cx, Scriptable scope,
                                    Object arg, int typeTag)
        {
            switch (typeTag)
            {
                case JAVA_STRING_TYPE:
                    if (arg is String)
                        return arg;
                    return ScriptRuntime.toString(arg);
                case JAVA_INT_TYPE:
                    if (arg is int)
                        return arg;
                    return ScriptRuntime.toInt32(arg);
                case JAVA_BOOLEAN_TYPE:
                    if (arg is Boolean)
                        return arg;
                    return ScriptRuntime.toBoolean(arg) ? true
                                                        : false;
                case JAVA_DOUBLE_TYPE:
                    if (arg is Double)
                        return arg;
                    return ScriptRuntime.toNumber(arg);
                case JAVA_SCRIPTABLE_TYPE:
                    return ScriptRuntime.toObjectOrNull(cx, arg, scope);
                case JAVA_OBJECT_TYPE:
                    return arg;
                default:
                    throw new ArgumentException();
            }
        }

        public FunctionObject(String name, MethodBase methodOrConstructor,
                         Scriptable scope)
        {
            if (methodOrConstructor is ConstructorInfo)
            {
                member = new MemberBox((ConstructorInfo)methodOrConstructor);
                isStatic = true; // well, doesn't take a 'this'
            }
            else
            {
                member = new MemberBox((MethodInfo)methodOrConstructor);
                isStatic = member.isStatic();
            }
            String methodName = member.getName();
            this.functionName = name;
            Type[] types = member.argTypes;
            int arity = types.Length;
            if (arity == 4 && (types[1].IsArray || types[2].IsArray))
            {
                // Either variable args or an error.
                if (types[1].IsArray)
                {
                    if (!isStatic ||
                        types[0] != ScriptRuntime.ContextClass ||
                        types[1].GetType() != ScriptRuntime.ObjectClass ||
                        types[2] != ScriptRuntime.FunctionClass ||
                        types[3] != typeof(Boolean))
                    {
                        throw Context.reportRuntimeError1(
                            "msg.varargs.ctor", methodName);
                    }
                    parmsLength = VARARGS_CTOR;
                }
                else
                {
                    if (!isStatic ||
                        types[0] != ScriptRuntime.ContextClass ||
                        types[1] != ScriptRuntime.ScriptableClass ||
                        types[2] != typeof(object[]) ||
                        types[3] != ScriptRuntime.FunctionClass)
                    {
                        throw Context.reportRuntimeError1(
                            "msg.varargs.fun", methodName);
                    }
                    parmsLength = VARARGS_METHOD;
                }
            }
            else
            {
                parmsLength = arity;
                if (arity > 0)
                {
                    typeTags = new byte[arity];
                    for (int i = 0; i != arity; ++i)
                    {
                        int tag = getTypeTag(types[i]);
                        if (tag == JAVA_UNSUPPORTED_TYPE)
                        {
                            throw Context.reportRuntimeError2(
                                "msg.bad.parms", types[i].Name, methodName);
                        }
                        typeTags[i] = (byte)tag;
                    }
                }
            }

            if (member.isMethod())
            {
                MethodInfo method = member.method();
                Type returnType = method.ReturnType;
                if (returnType == typeof(void))
                {
                    hasVoidReturn = true;
                }
                else
                {
                    returnTypeTag = getTypeTag(returnType);
                }
            }
            else
            {
                Type ctorType = member.getDeclaringClass();
                if (!ScriptRuntime.ScriptableClass.IsAssignableFrom(ctorType))
                {
                    throw Context.reportRuntimeError1(
                        "msg.bad.ctor.return", ctorType.Name);
                }
            }

            ScriptRuntime.setFunctionProtoAndParent(this, scope);
        }


        public static MethodInfo findSingleMethod(MethodInfo[] methods, String name)
        {
            MethodInfo found = null;
            for (int i = 0, N = methods.Length; i != N; ++i)
            {
                MethodInfo method = methods[i];
                if (method != null && name.Equals(method.Name))
                {
                    if (found != null)
                    {
                        throw Context.reportRuntimeError2(
                            "msg.no.overload", name,
                            method.DeclaringType.Name);
                    }
                    found = method;
                }
            }
            return found;
        }

        public static MethodInfo[] getMethodList(Type clazz)
        {
            MethodInfo[] methods = null;

            try
            {
                // getDeclaredMethods may be rejected by the security manager
                // but getMethods is more expensive
                if (!sawSecurityException)
                {

                    methods = clazz.GetMethods(BindingFlags.Public | BindingFlags.DeclaredOnly | BindingFlags.Static);
                }
            }
            catch (Exception e)
            {
                // If we get an exception once, give up on getDeclaredMethods
                sawSecurityException = true;
            }
            if (methods == null)
            {
                methods = clazz.GetMethods();
            }
            int count = 0;
            for (int i = 0; i < methods.Length; i++)
            {
                if (!methods[i].IsPublic)
                {
                    methods[i] = null;
                }
                else
                {
                    count++;
                }
            }
            MethodInfo[] result = new MethodInfo[count];
            int j = 0;
            for (int i = 0; i < methods.Length; i++)
            {
                if (methods[i] != null)
                    result[j++] = methods[i];
            }
            return result;
        }

        public static int getTypeTag(Type type)
        {
            if (type == ScriptRuntime.StringClass)
                return JAVA_STRING_TYPE;
            if (type == ScriptRuntime.IntegerClass || type == typeof(Int32))
                return JAVA_INT_TYPE;
            if (type == ScriptRuntime.BooleanClass || type == typeof(bool))
                return JAVA_BOOLEAN_TYPE;
            if (type == ScriptRuntime.DoubleClass || type == typeof(double))
                return JAVA_DOUBLE_TYPE;
            if (ScriptRuntime.ScriptableClass.IsAssignableFrom(type))
                return JAVA_SCRIPTABLE_TYPE;
            if (type == ScriptRuntime.ObjectClass)
                return JAVA_OBJECT_TYPE;

            // Note that the long type is not supported; see the javadoc for
            // the constructor for this class

            return JAVA_UNSUPPORTED_TYPE;
        }

        public override int getArity()
        {
            return parmsLength < 0 ? 1 : parmsLength;
        }

        public override int getLength()
        {
            return getArity();
        }

        public override string getFunctionName()
        {
            return (functionName == null) ? "" : functionName;
        }

        public MethodBase getMethodOrConstructor()
        {
            if (member.isMethod())
            {
                return member.method();
            }
            else
            {
                return member.ctor();
            }
        }

        public static MethodBase findSingleMethod(MethodBase[] methods, String name)
        {
            MethodBase found = null;
            for (int i = 0, N = methods.Length; i != N; ++i)
            {
                MethodBase method = methods[i];
                if (method != null && name == method.Name)
                {
                    if (found != null)
                    {
                        throw Context.reportRuntimeError2(
                            "msg.no.overload", name,
                            method.DeclaringType.Name);
                    }
                    found = method;
                }
            }
            return found;
        }


        //public static MethodBase[] getMethodList(Type clazz)
        //{
        //    MethodBase[] methods = null;
        //    try
        //    {
        //        // getDeclaredMethods may be rejected by the security manager
        //        // but getMethods is more expensive
        //        if (!sawSecurityException)
        //            methods = clazz.GetMethods(BindingFlags.Public | BindingFlags.DeclaredOnly | BindingFlags.Static);
        //    }
        //    catch (SecurityException e)
        //    {
        //        // If we get an exception once, give up on getDeclaredMethods
        //        sawSecurityException = true;
        //    }
        //    if (methods == null)
        //    {
        //        methods = clazz.GetMethods();
        //    }
        //    int count = 0;
        //    for (int i = 0; i < methods.Length; i++)
        //    {
        //        if (sawSecurityException
        //            ? methods[i].DeclaringType != clazz
        //            : !methods[i].IsPublic)
        //        {
        //            methods[i] = null;
        //        }
        //        else
        //        {
        //            count++;
        //        }
        //    }
        //    MethodBase[] result = new MethodBase[count];
        //    int j = 0;
        //    for (int i = 0; i < methods.Length; i++)
        //    {
        //        if (methods[i] != null)
        //            result[j++] = methods[i];
        //    }
        //    return result;
        //}

        public void addAsConstructor(Scriptable scope, Scriptable prototype)
        {
            initAsConstructor(scope, prototype);
            defineProperty(scope, prototype.getClassName(),
                           this, ScriptableObject.DONTENUM);
        }

        public void initAsConstructor(Scriptable scope, Scriptable prototype)
        {
            ScriptRuntime.setFunctionProtoAndParent(this, scope);
            setImmunePrototypeProperty(prototype);

            prototype.setParentScope(this);

            defineProperty(prototype, "constructor", this,
                           ScriptableObject.DONTENUM |
                           ScriptableObject.PERMANENT |
                           ScriptableObject.READONLY);
            setParentScope(scope);
        }

        public override Object call(Context cx, Scriptable scope, Scriptable thisObj,
                       Object[] args)
        {
            Object result;
            bool checkMethodResult = false;

            if (parmsLength < 0)
            {
                if (parmsLength == VARARGS_METHOD)
                {
                    Object[] invokeArgs = { cx, thisObj, args, this };
                    result = member.invoke(null, invokeArgs);
                    checkMethodResult = true;
                }
                else
                {
                    bool inNewExpr = (thisObj == null);
                    Boolean b = inNewExpr;
                    Object[] invokeArgs = { cx, args, this, b };
                    result = (member.isCtor())
                             ? member.newInstance(invokeArgs)
                             : member.invoke(null, invokeArgs);
                }

            }
            else
            {
                if (!isStatic)
                {
                    Type clazz = member.getDeclaringClass();

                    if (!clazz.IsInstanceOfType(thisObj))
                    {
                        bool compatible = false;
                        if (thisObj == scope)
                        {
                            Scriptable parentScope = getParentScope();
                            if (scope != parentScope)
                            {
                                // Call with dynamic scope for standalone function,
                                // use parentScope as thisObj
                                compatible = clazz.IsInstanceOfType(parentScope);
                                if (compatible)
                                {
                                    thisObj = parentScope;
                                }
                            }
                        }
                        if (!compatible)
                        {
                            // Couldn't find an object to call this on.
                            throw ScriptRuntime.typeError1("msg.incompat.call",
                                                           functionName);
                        }
                    }
                }

                Object[] invokeArgs;
                if (parmsLength == args.Length)
                {
                    // Do not allocate new argument array if java arguments are
                    // the same as the original js ones.
                    invokeArgs = args;
                    for (int i = 0; i != parmsLength; ++i)
                    {
                        Object arg = args[i];
                        Object converted = convertArg(cx, scope, arg, typeTags[i]);
                        if (arg != converted)
                        {
                            if (invokeArgs == args)
                            {
                                invokeArgs = (object[])args.Clone();
                            }
                            invokeArgs[i] = converted;
                        }
                    }
                }
                else if (parmsLength == 0)
                {
                    invokeArgs = ScriptRuntime.emptyArgs;
                }
                else
                {
                    invokeArgs = new Object[parmsLength];
                    for (int i = 0; i != parmsLength; ++i)
                    {
                        Object arg = (i < args.Length)
                                     ? args[i]
                                     : Undefined.instance;
                        invokeArgs[i] = convertArg(cx, scope, arg, typeTags[i]);
                    }
                }

                if (member.isMethod())
                {
                    result = member.invoke(thisObj, invokeArgs);
                    checkMethodResult = true;
                }
                else
                {
                    result = member.newInstance(invokeArgs);
                }

            }

            if (checkMethodResult)
            {
                if (hasVoidReturn)
                {
                    result = Undefined.instance;
                }
                else if (returnTypeTag == JAVA_UNSUPPORTED_TYPE)
                {
                    result = cx.getWrapFactory().wrap(cx, scope, result, null);
                }
                // XXX: the code assumes that if returnTypeTag == JAVA_OBJECT_TYPE
                // then the Java method did a proper job of converting the
                // result to JS primitive or Scriptable to avoid
                // potentially costly Context.javaToJS call.
            }

            return result;
        }

        //public Scriptable createObject(Context cx, Scriptable scope)
        //{
        //    Scriptable newInstance = new NativeObject();
        //    newInstance.setPrototype(getClassPrototype());
        //    newInstance.setParentScope(getParentScope());
        //    return newInstance;
        //}
        //public override cr
        public override Scriptable createObject(Context cx, Scriptable scope)
        {
            if (member.isCtor() || parmsLength == VARARGS_CTOR)
            {
                return null;
            }
            Scriptable result;
            try
            {
                Type t = member.getDeclaringClass();
                result = (Scriptable)t.Assembly.CreateInstance(t.Name);
            }
            catch (Exception ex)
            {
                throw Context.throwAsScriptRuntimeEx(ex);
            }

            result.setPrototype(getClassPrototype());
            result.setParentScope(getParentScope());
            return result;
        }

        public bool isVarArgsMethod()
        {
            return parmsLength == VARARGS_METHOD;
        }

        public bool isVarArgsConstructor()
        {
            return parmsLength == VARARGS_CTOR;
        }

        private static short VARARGS_METHOD = -1;
        private static short VARARGS_CTOR = -2;

        private static bool sawSecurityException;

        public const int JAVA_UNSUPPORTED_TYPE = 0;
        public const int JAVA_STRING_TYPE = 1;
        public const int JAVA_INT_TYPE = 2;
        public const int JAVA_BOOLEAN_TYPE = 3;
        public const int JAVA_DOUBLE_TYPE = 4;
        public const int JAVA_SCRIPTABLE_TYPE = 5;
        public const int JAVA_OBJECT_TYPE = 6;


        public MemberBox member;
        private String functionName;
        private byte[] typeTags;
        private int parmsLength;
        private bool hasVoidReturn;
        private int returnTypeTag;
        private bool isStatic;
    }
}
